{
 "cells": [
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:54:03.424798Z",
     "start_time": "2025-02-26T09:54:00.216215Z"
    }
   },
   "source": [
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "%matplotlib inline\n",
    "import numpy as np\n",
    "import sklearn\n",
    "import pandas as pd\n",
    "import os\n",
    "import sys\n",
    "import time\n",
    "from tqdm.auto import tqdm\n",
    "import torch\n",
    "import torch.nn as nn\n",
    "import torch.nn.functional as F\n",
    "\n",
    "print(sys.version_info)\n",
    "for module in mpl, np, pd, sklearn, torch:\n",
    "    print(module.__name__, module.__version__)\n",
    "    \n",
    "device = torch.device(\"cuda:0\") if torch.cuda.is_available() else torch.device(\"cpu\")\n",
    "print(device)\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sys.version_info(major=3, minor=12, micro=3, releaselevel='final', serial=0)\n",
      "matplotlib 3.10.0\n",
      "numpy 2.0.2\n",
      "pandas 2.2.3\n",
      "sklearn 1.6.1\n",
      "torch 2.6.0+cu126\n",
      "cuda:0\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 准备数据"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:54:13.843917Z",
     "start_time": "2025-02-26T09:54:13.834395Z"
    }
   },
   "source": [
    "from sklearn.datasets import fetch_california_housing\n",
    "\n",
    "housing = fetch_california_housing(data_home='data')\n",
    "print(housing.DESCR)\n",
    "print(housing.data.shape)\n",
    "print(housing.target.shape)"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      ".. _california_housing_dataset:\n",
      "\n",
      "California Housing dataset\n",
      "--------------------------\n",
      "\n",
      "**Data Set Characteristics:**\n",
      "\n",
      ":Number of Instances: 20640\n",
      "\n",
      ":Number of Attributes: 8 numeric, predictive attributes and the target\n",
      "\n",
      ":Attribute Information:\n",
      "    - MedInc        median income in block group\n",
      "    - HouseAge      median house age in block group\n",
      "    - AveRooms      average number of rooms per household\n",
      "    - AveBedrms     average number of bedrooms per household\n",
      "    - Population    block group population\n",
      "    - AveOccup      average number of household members\n",
      "    - Latitude      block group latitude\n",
      "    - Longitude     block group longitude\n",
      "\n",
      ":Missing Attribute Values: None\n",
      "\n",
      "This dataset was obtained from the StatLib repository.\n",
      "https://www.dcc.fc.up.pt/~ltorgo/Regression/cal_housing.html\n",
      "\n",
      "The target variable is the median house value for California districts,\n",
      "expressed in hundreds of thousands of dollars ($100,000).\n",
      "\n",
      "This dataset was derived from the 1990 U.S. census, using one row per census\n",
      "block group. A block group is the smallest geographical unit for which the U.S.\n",
      "Census Bureau publishes sample data (a block group typically has a population\n",
      "of 600 to 3,000 people).\n",
      "\n",
      "A household is a group of people residing within a home. Since the average\n",
      "number of rooms and bedrooms in this dataset are provided per household, these\n",
      "columns may take surprisingly large values for block groups with few households\n",
      "and many empty houses, such as vacation resorts.\n",
      "\n",
      "It can be downloaded/loaded using the\n",
      ":func:`sklearn.datasets.fetch_california_housing` function.\n",
      "\n",
      ".. rubric:: References\n",
      "\n",
      "- Pace, R. Kelley and Ronald Barry, Sparse Spatial Autoregressions,\n",
      "  Statistics and Probability Letters, 33 (1997) 291-297\n",
      "\n",
      "(20640, 8)\n",
      "(20640,)\n"
     ]
    }
   ],
   "execution_count": 3
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:54:16.005833Z",
     "start_time": "2025-02-26T09:54:16.001832Z"
    }
   },
   "source": [
    "# print(housing.data[0:5])\n",
    "import pprint  #打印的格式比较 好看\n",
    "\n",
    "pprint.pprint(housing.data[0:5])\n",
    "print('-'*50)\n",
    "pprint.pprint(housing.target[0:5])"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "array([[ 8.32520000e+00,  4.10000000e+01,  6.98412698e+00,\n",
      "         1.02380952e+00,  3.22000000e+02,  2.55555556e+00,\n",
      "         3.78800000e+01, -1.22230000e+02],\n",
      "       [ 8.30140000e+00,  2.10000000e+01,  6.23813708e+00,\n",
      "         9.71880492e-01,  2.40100000e+03,  2.10984183e+00,\n",
      "         3.78600000e+01, -1.22220000e+02],\n",
      "       [ 7.25740000e+00,  5.20000000e+01,  8.28813559e+00,\n",
      "         1.07344633e+00,  4.96000000e+02,  2.80225989e+00,\n",
      "         3.78500000e+01, -1.22240000e+02],\n",
      "       [ 5.64310000e+00,  5.20000000e+01,  5.81735160e+00,\n",
      "         1.07305936e+00,  5.58000000e+02,  2.54794521e+00,\n",
      "         3.78500000e+01, -1.22250000e+02],\n",
      "       [ 3.84620000e+00,  5.20000000e+01,  6.28185328e+00,\n",
      "         1.08108108e+00,  5.65000000e+02,  2.18146718e+00,\n",
      "         3.78500000e+01, -1.22250000e+02]])\n",
      "--------------------------------------------------\n",
      "array([4.526, 3.585, 3.521, 3.413, 3.422])\n"
     ]
    }
   ],
   "execution_count": 4
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:54:16.760165Z",
     "start_time": "2025-02-26T09:54:16.718863Z"
    }
   },
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "#拆分训练集和测试集，random_state是随机种子,同样的随机数种子，是为了得到同样的随机值\n",
    "x_train_all, x_test, y_train_all, y_test = train_test_split(\n",
    "    housing.data, housing.target, random_state = 7)\n",
    "x_train, x_valid, y_train, y_valid = train_test_split(\n",
    "    x_train_all, y_train_all, random_state = 11)\n",
    "# 训练集\n",
    "print(x_train.shape, y_train.shape)\n",
    "# 验证集\n",
    "print(x_valid.shape, y_valid.shape)\n",
    "# 测试集\n",
    "print(x_test.shape, y_test.shape)\n",
    "\n",
    "dataset_maps = {\n",
    "    \"train\": [x_train, y_train],\n",
    "    \"valid\": [x_valid, y_valid],\n",
    "    \"test\": [x_test, y_test],\n",
    "}\n"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "(11610, 8) (11610,)\n",
      "(3870, 8) (3870,)\n",
      "(5160, 8) (5160,)\n"
     ]
    }
   ],
   "execution_count": 5
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:54:17.525865Z",
     "start_time": "2025-02-26T09:54:17.517847Z"
    }
   },
   "source": [
    "from sklearn.preprocessing import StandardScaler\n",
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "scaler = StandardScaler()\n",
    "scaler.fit(x_train)"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "StandardScaler()"
      ],
      "text/html": [
       "<style>#sk-container-id-1 {\n",
       "  /* Definition of color scheme common for light and dark mode */\n",
       "  --sklearn-color-text: #000;\n",
       "  --sklearn-color-text-muted: #666;\n",
       "  --sklearn-color-line: gray;\n",
       "  /* Definition of color scheme for unfitted estimators */\n",
       "  --sklearn-color-unfitted-level-0: #fff5e6;\n",
       "  --sklearn-color-unfitted-level-1: #f6e4d2;\n",
       "  --sklearn-color-unfitted-level-2: #ffe0b3;\n",
       "  --sklearn-color-unfitted-level-3: chocolate;\n",
       "  /* Definition of color scheme for fitted estimators */\n",
       "  --sklearn-color-fitted-level-0: #f0f8ff;\n",
       "  --sklearn-color-fitted-level-1: #d4ebff;\n",
       "  --sklearn-color-fitted-level-2: #b3dbfd;\n",
       "  --sklearn-color-fitted-level-3: cornflowerblue;\n",
       "\n",
       "  /* Specific color for light theme */\n",
       "  --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, white)));\n",
       "  --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, black)));\n",
       "  --sklearn-color-icon: #696969;\n",
       "\n",
       "  @media (prefers-color-scheme: dark) {\n",
       "    /* Redefinition of color scheme for dark theme */\n",
       "    --sklearn-color-text-on-default-background: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-background: var(--sg-background-color, var(--theme-background, var(--jp-layout-color0, #111)));\n",
       "    --sklearn-color-border-box: var(--sg-text-color, var(--theme-code-foreground, var(--jp-content-font-color1, white)));\n",
       "    --sklearn-color-icon: #878787;\n",
       "  }\n",
       "}\n",
       "\n",
       "#sk-container-id-1 {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 pre {\n",
       "  padding: 0;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-hidden--visually {\n",
       "  border: 0;\n",
       "  clip: rect(1px 1px 1px 1px);\n",
       "  clip: rect(1px, 1px, 1px, 1px);\n",
       "  height: 1px;\n",
       "  margin: -1px;\n",
       "  overflow: hidden;\n",
       "  padding: 0;\n",
       "  position: absolute;\n",
       "  width: 1px;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-dashed-wrapped {\n",
       "  border: 1px dashed var(--sklearn-color-line);\n",
       "  margin: 0 0.4em 0.5em 0.4em;\n",
       "  box-sizing: border-box;\n",
       "  padding-bottom: 0.4em;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-container {\n",
       "  /* jupyter's `normalize.less` sets `[hidden] { display: none; }`\n",
       "     but bootstrap.min.css set `[hidden] { display: none !important; }`\n",
       "     so we also need the `!important` here to be able to override the\n",
       "     default hidden behavior on the sphinx rendered scikit-learn.org.\n",
       "     See: https://github.com/scikit-learn/scikit-learn/issues/21755 */\n",
       "  display: inline-block !important;\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-text-repr-fallback {\n",
       "  display: none;\n",
       "}\n",
       "\n",
       "div.sk-parallel-item,\n",
       "div.sk-serial,\n",
       "div.sk-item {\n",
       "  /* draw centered vertical line to link estimators */\n",
       "  background-image: linear-gradient(var(--sklearn-color-text-on-default-background), var(--sklearn-color-text-on-default-background));\n",
       "  background-size: 2px 100%;\n",
       "  background-repeat: no-repeat;\n",
       "  background-position: center center;\n",
       "}\n",
       "\n",
       "/* Parallel-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item::after {\n",
       "  content: \"\";\n",
       "  width: 100%;\n",
       "  border-bottom: 2px solid var(--sklearn-color-text-on-default-background);\n",
       "  flex-grow: 1;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel {\n",
       "  display: flex;\n",
       "  align-items: stretch;\n",
       "  justify-content: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  position: relative;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:first-child::after {\n",
       "  align-self: flex-end;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:last-child::after {\n",
       "  align-self: flex-start;\n",
       "  width: 50%;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-parallel-item:only-child::after {\n",
       "  width: 0;\n",
       "}\n",
       "\n",
       "/* Serial-specific style estimator block */\n",
       "\n",
       "#sk-container-id-1 div.sk-serial {\n",
       "  display: flex;\n",
       "  flex-direction: column;\n",
       "  align-items: center;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  padding-right: 1em;\n",
       "  padding-left: 1em;\n",
       "}\n",
       "\n",
       "\n",
       "/* Toggleable style: style used for estimator/Pipeline/ColumnTransformer box that is\n",
       "clickable and can be expanded/collapsed.\n",
       "- Pipeline and ColumnTransformer use this feature and define the default style\n",
       "- Estimators will overwrite some part of the style using the `sk-estimator` class\n",
       "*/\n",
       "\n",
       "/* Pipeline and ColumnTransformer style (default) */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable {\n",
       "  /* Default theme specific background. It is overwritten whether we have a\n",
       "  specific estimator or a Pipeline/ColumnTransformer */\n",
       "  background-color: var(--sklearn-color-background);\n",
       "}\n",
       "\n",
       "/* Toggleable label */\n",
       "#sk-container-id-1 label.sk-toggleable__label {\n",
       "  cursor: pointer;\n",
       "  display: flex;\n",
       "  width: 100%;\n",
       "  margin-bottom: 0;\n",
       "  padding: 0.5em;\n",
       "  box-sizing: border-box;\n",
       "  text-align: center;\n",
       "  align-items: start;\n",
       "  justify-content: space-between;\n",
       "  gap: 0.5em;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label .caption {\n",
       "  font-size: 0.6rem;\n",
       "  font-weight: lighter;\n",
       "  color: var(--sklearn-color-text-muted);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:before {\n",
       "  /* Arrow on the left of the label */\n",
       "  content: \"▸\";\n",
       "  float: left;\n",
       "  margin-right: 0.25em;\n",
       "  color: var(--sklearn-color-icon);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 label.sk-toggleable__label-arrow:hover:before {\n",
       "  color: var(--sklearn-color-text);\n",
       "}\n",
       "\n",
       "/* Toggleable content - dropdown */\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content {\n",
       "  max-height: 0;\n",
       "  max-width: 0;\n",
       "  overflow: hidden;\n",
       "  text-align: left;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content pre {\n",
       "  margin: 0.2em;\n",
       "  border-radius: 0.25em;\n",
       "  color: var(--sklearn-color-text);\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-toggleable__content.fitted pre {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~div.sk-toggleable__content {\n",
       "  /* Expand drop-down */\n",
       "  max-height: 200px;\n",
       "  max-width: 100%;\n",
       "  overflow: auto;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 input.sk-toggleable__control:checked~label.sk-toggleable__label-arrow:before {\n",
       "  content: \"▾\";\n",
       "}\n",
       "\n",
       "/* Pipeline/ColumnTransformer-specific style */\n",
       "\n",
       "#sk-container-id-1 div.sk-label input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator-specific style */\n",
       "\n",
       "/* Colorize estimator box */\n",
       "#sk-container-id-1 div.sk-estimator input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted input.sk-toggleable__control:checked~label.sk-toggleable__label {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label label.sk-toggleable__label,\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  /* The background is the default theme color */\n",
       "  color: var(--sklearn-color-text-on-default-background);\n",
       "}\n",
       "\n",
       "/* On hover, darken the color of the background */\n",
       "#sk-container-id-1 div.sk-label:hover label.sk-toggleable__label {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "/* Label box, darken color on hover, fitted */\n",
       "#sk-container-id-1 div.sk-label.fitted:hover label.sk-toggleable__label.fitted {\n",
       "  color: var(--sklearn-color-text);\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Estimator label */\n",
       "\n",
       "#sk-container-id-1 div.sk-label label {\n",
       "  font-family: monospace;\n",
       "  font-weight: bold;\n",
       "  display: inline-block;\n",
       "  line-height: 1.2em;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-label-container {\n",
       "  text-align: center;\n",
       "}\n",
       "\n",
       "/* Estimator-specific */\n",
       "#sk-container-id-1 div.sk-estimator {\n",
       "  font-family: monospace;\n",
       "  border: 1px dotted var(--sklearn-color-border-box);\n",
       "  border-radius: 0.25em;\n",
       "  box-sizing: border-box;\n",
       "  margin-bottom: 0.5em;\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-0);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-0);\n",
       "}\n",
       "\n",
       "/* on hover */\n",
       "#sk-container-id-1 div.sk-estimator:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-2);\n",
       "}\n",
       "\n",
       "#sk-container-id-1 div.sk-estimator.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-2);\n",
       "}\n",
       "\n",
       "/* Specification for estimator info (e.g. \"i\" and \"?\") */\n",
       "\n",
       "/* Common style for \"i\" and \"?\" */\n",
       "\n",
       ".sk-estimator-doc-link,\n",
       "a:link.sk-estimator-doc-link,\n",
       "a:visited.sk-estimator-doc-link {\n",
       "  float: right;\n",
       "  font-size: smaller;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1em;\n",
       "  height: 1em;\n",
       "  width: 1em;\n",
       "  text-decoration: none !important;\n",
       "  margin-left: 0.5em;\n",
       "  text-align: center;\n",
       "  /* unfitted */\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted,\n",
       "a:link.sk-estimator-doc-link.fitted,\n",
       "a:visited.sk-estimator-doc-link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "div.sk-estimator:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link:hover,\n",
       ".sk-estimator-doc-link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "div.sk-estimator.fitted:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover,\n",
       "div.sk-label-container:hover .sk-estimator-doc-link.fitted:hover,\n",
       ".sk-estimator-doc-link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "/* Span, style for the box shown on hovering the info icon */\n",
       ".sk-estimator-doc-link span {\n",
       "  display: none;\n",
       "  z-index: 9999;\n",
       "  position: relative;\n",
       "  font-weight: normal;\n",
       "  right: .2ex;\n",
       "  padding: .5ex;\n",
       "  margin: .5ex;\n",
       "  width: min-content;\n",
       "  min-width: 20ex;\n",
       "  max-width: 50ex;\n",
       "  color: var(--sklearn-color-text);\n",
       "  box-shadow: 2pt 2pt 4pt #999;\n",
       "  /* unfitted */\n",
       "  background: var(--sklearn-color-unfitted-level-0);\n",
       "  border: .5pt solid var(--sklearn-color-unfitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link.fitted span {\n",
       "  /* fitted */\n",
       "  background: var(--sklearn-color-fitted-level-0);\n",
       "  border: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "\n",
       ".sk-estimator-doc-link:hover span {\n",
       "  display: block;\n",
       "}\n",
       "\n",
       "/* \"?\"-specific style due to the `<a>` HTML tag */\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link {\n",
       "  float: right;\n",
       "  font-size: 1rem;\n",
       "  line-height: 1em;\n",
       "  font-family: monospace;\n",
       "  background-color: var(--sklearn-color-background);\n",
       "  border-radius: 1rem;\n",
       "  height: 1rem;\n",
       "  width: 1rem;\n",
       "  text-decoration: none;\n",
       "  /* unfitted */\n",
       "  color: var(--sklearn-color-unfitted-level-1);\n",
       "  border: var(--sklearn-color-unfitted-level-1) 1pt solid;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted {\n",
       "  /* fitted */\n",
       "  border: var(--sklearn-color-fitted-level-1) 1pt solid;\n",
       "  color: var(--sklearn-color-fitted-level-1);\n",
       "}\n",
       "\n",
       "/* On hover */\n",
       "#sk-container-id-1 a.estimator_doc_link:hover {\n",
       "  /* unfitted */\n",
       "  background-color: var(--sklearn-color-unfitted-level-3);\n",
       "  color: var(--sklearn-color-background);\n",
       "  text-decoration: none;\n",
       "}\n",
       "\n",
       "#sk-container-id-1 a.estimator_doc_link.fitted:hover {\n",
       "  /* fitted */\n",
       "  background-color: var(--sklearn-color-fitted-level-3);\n",
       "}\n",
       "</style><div id=\"sk-container-id-1\" class=\"sk-top-container\"><div class=\"sk-text-repr-fallback\"><pre>StandardScaler()</pre><b>In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook. <br />On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.</b></div><div class=\"sk-container\" hidden><div class=\"sk-item\"><div class=\"sk-estimator fitted sk-toggleable\"><input class=\"sk-toggleable__control sk-hidden--visually\" id=\"sk-estimator-id-1\" type=\"checkbox\" checked><label for=\"sk-estimator-id-1\" class=\"sk-toggleable__label fitted sk-toggleable__label-arrow\"><div><div>StandardScaler</div></div><div><a class=\"sk-estimator-doc-link fitted\" rel=\"noreferrer\" target=\"_blank\" href=\"https://scikit-learn.org/1.6/modules/generated/sklearn.preprocessing.StandardScaler.html\">?<span>Documentation for StandardScaler</span></a><span class=\"sk-estimator-doc-link fitted\">i<span>Fitted</span></span></div></label><div class=\"sk-toggleable__content fitted\"><pre>StandardScaler()</pre></div> </div></div></div></div>"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 6
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 构建数据集"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:54:18.758725Z",
     "start_time": "2025-02-26T09:54:18.749652Z"
    }
   },
   "source": [
    "from torch.utils.data import Dataset\n",
    "\n",
    "class HousingDataset(Dataset):\n",
    "    def __init__(self, mode='train'):\n",
    "        self.x, self.y = dataset_maps[mode]\n",
    "        self.x = torch.from_numpy(scaler.transform(self.x)).float()\n",
    "        self.y = torch.from_numpy(self.y).float().reshape(-1, 1)\n",
    "            \n",
    "    def __len__(self):\n",
    "        return len(self.x)\n",
    "    \n",
    "    def __getitem__(self, idx):\n",
    "        return self.x[idx], self.y[idx]\n",
    "    \n",
    "    \n",
    "train_ds = HousingDataset(\"train\")\n",
    "valid_ds = HousingDataset(\"valid\")\n",
    "test_ds = HousingDataset(\"test\")"
   ],
   "outputs": [],
   "execution_count": 7
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:54:19.597542Z",
     "start_time": "2025-02-26T09:54:19.590652Z"
    }
   },
   "source": [
    "train_ds[1]"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "(tensor([-0.2981,  0.3523, -0.1092, -0.2506, -0.0341, -0.0060,  1.0806, -1.0611]),\n",
       " tensor([1.5140]))"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 8
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### DataLoader"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:54:20.242954Z",
     "start_time": "2025-02-26T09:54:20.239440Z"
    }
   },
   "source": [
    "from torch.utils.data import DataLoader\n",
    "\n",
    "\n",
    "batch_size = 8\n",
    "train_loader = DataLoader(train_ds, batch_size=batch_size, shuffle=False)\n",
    "val_loader = DataLoader(valid_ds, batch_size=batch_size, shuffle=False)\n",
    "test_loader = DataLoader(test_ds, batch_size=batch_size, shuffle=False)"
   ],
   "outputs": [],
   "execution_count": 9
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 定义模型\n",
    "\n",
    "多输出模型常见于多任务学习（Multi-Task Learning）中，一个任务会有一个输出层，计算一个loss，最后多个任务的loss联合训练模型。除此之外，如果想拿到模型的中间输出，也可以使用多输出。\n",
    "\n",
    "例如，这里想拿到 deep_output，我们构建多输出模型"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:56:31.625545Z",
     "start_time": "2025-02-26T09:56:31.621035Z"
    }
   },
   "source": [
    "#回归模型我们只需要1个数\n",
    "\n",
    "class WideDeep(nn.Module):\n",
    "    def __init__(self, input_dim=8):\n",
    "        super().__init__()\n",
    "        self.deep = nn.Sequential(\n",
    "            nn.Linear(input_dim, 30),\n",
    "            nn.ReLU(),\n",
    "            nn.Linear(30, 30),\n",
    "            nn.ReLU()\n",
    "            )\n",
    "        # pytorch 需要自行计算输出输出维度\n",
    "        self.output_layer = nn.Linear(30 + input_dim, 1)\n",
    "        \n",
    "        # 初始化权重\n",
    "        self.init_weights()\n",
    "        \n",
    "    def init_weights(self):\n",
    "        \"\"\"使用 xavier 均匀分布来初始化全连接层的权重 W\"\"\"\n",
    "        for m in self.modules():\n",
    "            if isinstance(m, nn.Linear):\n",
    "                nn.init.xavier_uniform_(m.weight)\n",
    "                nn.init.zeros_(m.bias)\n",
    "        \n",
    "    def forward(self, x, return_deep_output=False):\n",
    "        # x.shape [batch size, 8]\n",
    "        deep_output = self.deep(x)\n",
    "        # concat [batch size, 30] with [batch size 8]\n",
    "        concat = torch.cat([x, deep_output], dim=-1)\n",
    "        logits = self.output_layer(concat)\n",
    "        # logits.shape [batch size, 1]\n",
    "        return (logits, deep_output) if return_deep_output else logits #如果return_deep_output为True，返回logits和deep_output，否则只返回logits"
   ],
   "outputs": [],
   "execution_count": 10
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:56:31.974654Z",
     "start_time": "2025-02-26T09:56:31.970089Z"
    }
   },
   "source": [
    "class EarlyStopCallback:\n",
    "    def __init__(self, patience=5, min_delta=0.01):\n",
    "        \"\"\"\n",
    "\n",
    "        Args:\n",
    "            patience (int, optional): Number of epochs with no improvement after which training will be stopped.. Defaults to 5.\n",
    "            min_delta (float, optional): Minimum change in the monitored quantity to qualify as an improvement, i.e. an absolute \n",
    "                change of less than min_delta, will count as no improvement. Defaults to 0.01.\n",
    "        \"\"\"\n",
    "        self.patience = patience\n",
    "        self.min_delta = min_delta\n",
    "        self.best_metric = -1\n",
    "        self.counter = 0\n",
    "        \n",
    "    def __call__(self, metric):\n",
    "        if metric >= self.best_metric + self.min_delta:\n",
    "            # update best metric\n",
    "            self.best_metric = metric\n",
    "            # reset counter \n",
    "            self.counter = 0\n",
    "        else: \n",
    "            self.counter += 1\n",
    "            \n",
    "    @property\n",
    "    def early_stop(self):\n",
    "        return self.counter >= self.patience\n"
   ],
   "outputs": [],
   "execution_count": 11
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:56:32.389199Z",
     "start_time": "2025-02-26T09:56:32.384686Z"
    }
   },
   "source": [
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "@torch.no_grad()\n",
    "def evaluating(model, dataloader, loss_fct):\n",
    "    loss_list = []\n",
    "    for datas, labels in dataloader:\n",
    "        datas = datas.to(device)\n",
    "        labels = labels.to(device)\n",
    "        # 前向计算\n",
    "        logits = model(datas)\n",
    "        loss = loss_fct(logits, labels)         # 验证集损失\n",
    "        loss_list.append(loss.item())\n",
    "        \n",
    "    return np.mean(loss_list)\n"
   ],
   "outputs": [],
   "execution_count": 12
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": "## 训练(不重要，不用花时间看）"
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:57:07.959350Z",
     "start_time": "2025-02-26T09:56:33.015026Z"
    }
   },
   "source": [
    "# 训练\n",
    "def training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    tensorboard_callback=None,\n",
    "    save_ckpt_callback=None,\n",
    "    early_stop_callback=None,\n",
    "    eval_step=500,\n",
    "    ):\n",
    "    record_dict = {\n",
    "        \"train\": [],\n",
    "        \"val\": []\n",
    "    }\n",
    "    \n",
    "    global_step = 0\n",
    "    model.train()\n",
    "    with tqdm(total=epoch * len(train_loader)) as pbar:\n",
    "        for epoch_id in range(epoch):\n",
    "            # training\n",
    "            for datas, labels in train_loader:\n",
    "                datas = datas.to(device)\n",
    "                labels = labels.to(device)\n",
    "                # 梯度清空\n",
    "                optimizer.zero_grad()\n",
    "                # 模型前向计算\n",
    "                logits, deep_output = model(datas,return_deep_output=True)\n",
    "                deep_output= deep_output.mean(axis=1).reshape(-1, 1) # # 平均池化,尺寸为[batch size, 30]，求平均就变为[batch size],reshape成[batch size, 1]\n",
    "                logits=logits+deep_output  # 尺寸一致，相加，求损失\n",
    "                # 计算损失\n",
    "                loss = loss_fct(logits, labels)\n",
    "                # 梯度回传\n",
    "                loss.backward()\n",
    "                # 调整优化器，包括学习率的变动等\n",
    "                optimizer.step()\n",
    " \n",
    "                loss = loss.cpu().item()\n",
    "                # record\n",
    "                \n",
    "                record_dict[\"train\"].append({\n",
    "                    \"loss\": loss, \"step\": global_step\n",
    "                })\n",
    "                \n",
    "                # evaluating\n",
    "                if global_step % eval_step == 0:\n",
    "                    model.eval()\n",
    "                    val_loss = evaluating(model, val_loader, loss_fct)\n",
    "                    record_dict[\"val\"].append({\n",
    "                        \"loss\": val_loss, \"step\": global_step\n",
    "                    })\n",
    "                    model.train()\n",
    "\n",
    "                    # 早停 Early Stop\n",
    "                    if early_stop_callback is not None:\n",
    "                        early_stop_callback(-val_loss)\n",
    "                        if early_stop_callback.early_stop:\n",
    "                            print(f\"Early stop at epoch {epoch_id} / global_step {global_step}\")\n",
    "                            return record_dict\n",
    "                    \n",
    "                # udate step\n",
    "                global_step += 1\n",
    "                pbar.update(1)\n",
    "                pbar.set_postfix({\"epoch\": epoch_id})\n",
    "        \n",
    "    return record_dict\n",
    "        \n",
    "\n",
    "epoch = 10\n",
    "\n",
    "model = WideDeep()\n",
    "\n",
    "# 1. 定义损失函数 采用MSE损失\n",
    "loss_fct = nn.MSELoss()\n",
    "# 2. 定义优化器 采用SGD\n",
    "# Optimizers specified in the torch.optim package\n",
    "optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.0)\n",
    "\n",
    "# 3. early stop\n",
    "early_stop_callback = EarlyStopCallback(patience=10, min_delta=1e-3)\n",
    "\n",
    "model = model.to(device)\n",
    "record = training(\n",
    "    model, \n",
    "    train_loader, \n",
    "    val_loader, \n",
    "    epoch, \n",
    "    loss_fct, \n",
    "    optimizer, \n",
    "    early_stop_callback=early_stop_callback,\n",
    "    eval_step=len(train_loader)\n",
    "    )"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "  0%|          | 0/14520 [00:00<?, ?it/s]"
      ],
      "application/vnd.jupyter.widget-view+json": {
       "version_major": 2,
       "version_minor": 0,
       "model_id": "a10c3d4e9b364be4bfb169217a6ca0fc"
      }
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "IOPub message rate exceeded.\n",
      "The Jupyter server will temporarily stop sending output\n",
      "to the client in order to avoid crashing it.\n",
      "To change this limit, set the config variable\n",
      "`--ServerApp.iopub_msg_rate_limit`.\n",
      "\n",
      "Current values:\n",
      "ServerApp.iopub_msg_rate_limit=1000.0 (msgs/sec)\n",
      "ServerApp.rate_limit_window=3.0 (secs)\n",
      "\n"
     ]
    }
   ],
   "execution_count": 13
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:58:36.773755Z",
     "start_time": "2025-02-26T09:58:36.680691Z"
    }
   },
   "source": [
    "#画线要注意的是损失是不一定在零到1之间的\n",
    "def plot_learning_curves(record_dict, sample_step=500):\n",
    "    # build DataFrame\n",
    "    train_df = pd.DataFrame(record_dict[\"train\"]).set_index(\"step\").iloc[::sample_step]\n",
    "    val_df = pd.DataFrame(record_dict[\"val\"]).set_index(\"step\")\n",
    "\n",
    "    # plot\n",
    "    for idx, item in enumerate(train_df.columns):\n",
    "        plt.plot(train_df.index, train_df[item], label=f\"train_{item}\")\n",
    "        plt.plot(val_df.index, val_df[item], label=f\"val_{item}\")\n",
    "        plt.grid()\n",
    "        plt.legend()\n",
    "        # plt.xticks(range(0, train_df.index[-1], 10*sample_step), range(0, train_df.index[-1], 10*sample_step))\n",
    "        plt.xlabel(\"step\")\n",
    "\n",
    "        plt.show()\n",
    "\n",
    "plot_learning_curves(record, sample_step=500)  #横坐标是 steps"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ],
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAh8AAAGwCAYAAAAJ/wd3AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAVtdJREFUeJzt3Qd4VFXaB/D/THomCSWhE0LvvQooojQ72BVU1F0rrHVddV0RxO7qYsH+KTYEV0VdxYKo9N4FpHcIECC9Z+73vGfmDpOQMklm7pT7/z3PZUouM/dMJnPfec97zrFomqaBiIiIyCBWo56IiIiISDD4ICIiIkMx+CAiIiJDMfggIiIiQzH4ICIiIkMx+CAiIiJDMfggIiIiQ4UjwNjtdhw+fBjx8fGwWCz+PhwiIiLygEwblpWVhaZNm8JqtQZX8CGBR3Jysr8Pg4iIiGrgwIEDaN68eXAFH5Lx0A8+ISHBq49dVFSEn3/+GSNHjkRERATMgu1mu83CrG1nu9nuQJCZmamSB/p5PKiCD72rRQIPXwQfsbGx6nED6Rfma2w3220WZm072812BxJPSiZYcEpERESGYvBBREREhmLwQURERIYKuJoPIiIKTSUlJapewVvkscLDw5Gfn68e2yyK/NjuyMjIKofReoLBBxER+Xz+h9TUVKSnp3v9cRs3bqxGR5ppXijNj+2WwKNVq1YqCKkNBh9ERORTeuDRsGFDNUrDWydMmZQyOzsbcXFxXvk2Hizsfmq3PgnokSNH0KJFi1r9Hhl8EBGRz0i3gB54JCYmev1kWFhYiOjoaNMFH4V+aneDBg1UAFJcXFyrYb7m+W0REZHh9BoPyXhQ8It0drfUttaEwQcREfmcmWoyQpnFS79HBh9ERERkKAYfREREZCgGH0RERD7WsmVLTJs2zSuP9fvvv6NevXpeH7psJNOMdikusSM1Mx9p+f4+EiIiCgZDhw5Fz549vRI0rFq1CjabzSvHFQpMk/nYdTwH57y4EP/ZFObvQyEiohCZ7EuGnHo6RJUjfkwYfNiiHEFHgd3fR0JEZG5y0s4tLPbKlldYUq395bk9cfPNN2PBggV45ZVX1AgP2WbMmKEuf/jhB/Tp0wdRUVFYvHgxdu3ahdGjR6NRo0Zq4q9+/frhl19+qbTbxWKx4L333sPll1+ugpJ27drh22+/rfFr+uWXX6JLly7qmOS5XnrppVI/f+ONN9RzyNwgcpxXXXWV62dffPEFunXrhpiYGDUXy/Dhw5GTkwNfMk23iy3S0dQiuwUldg01nxqFiIhqI6+oBJ0n/eSX597y5CjEOs8HlZGgY/v27ejatSuefPJJdd/mzZvV5SOPPIJ///vfaN26taq9kGnOL7roIjz99NPq5P/RRx/h0ksvxbZt29RMoBWZMmUKXnjhBbz44ot47bXXMG7cOOzbtw/169evVpvWrFmDa665BpMnT8a1116LpUuX4u6771aBhARRq1evxj333IOPP/4YgwYNwsmTJ7Fo0SL1f2W20uuvv14dhwRCWVlZ6meeBmk1ZZrgI9aZ+RC5hSWIjvLr4RARUQCrU6eOmlBLshKyjor4888/1aUEIyNGjHDtK8FCjx49XLenTp2KOXPmqEzGxIkTK3yOm2++WZ34xTPPPINXX30VK1euxAUXXFCtY3355ZcxbNgwPP744+p2+/btsWXLFhXUyHPs379f1ZtccskliI+PR0pKCnr16uUKPqTr6IorrlD3C8mC+Jppgo/IMCvCrRYU2x3pvurFlURE5C0xEWEqA+GNacazMrMQnxDv8TTj8ty11bdv31K3ZZ0VyTp8//33rpN5Xl6eOulXpnv37q7rEhwkJCTg2LFj1T6erVu3qm4fd4MHD1bdPDITqQRKElhIpkYCG9n07h4JmiRwkYBj1KhRGDlypOqSkYyOL5mm5kP612IjHW+6nALzLL1MRBSYn8fhXtliIsOqtb83ZugsO2rl73//u8p0SPZCuizWr1+vTuay/kplIsqsjSLHJgGVt0m2Y+3atfjss8/QpEkTTJo0SQUdMlQ3LCwM8+bNU3UsnTt3Vt0/HTp0wJ49e+BLpgk+hB58SLcLERFRZaTbxZM1TJYsWaK6NySbIEGHdNPs3bsXRunUqZM6hrLHJN0vElyI8PBwVUgqtR0bN25Ux/frr7+6gh7JlEgNyrp161S7JZjyJdN0uwhblDS3ADmFng2NIiIi85JRIytWrFAnahnFUlFWQkaRfPXVV6rIVE7kUnvhiwxGRR588EE1wkZqTaTgdNmyZXj99dfVCBfx3XffYffu3RgyZIjqTpk7d646PslwSPvmz5+vultk5WG5ffz4cRXQ+JKpMh82Zj6IiMhD0p0imQPpjpB5Oiqq4ZCCTzmpy0gSCUCkdqJ3796GHWfv3r3x+eefY9asWWp0jnSrSFGsZGNE3bp1VXB0/vnnq6DirbfeUl0wMjRX6kwWLlyoRutIpuRf//qXGqZ74YUX+vSYTZX5YLcLERF5Sk7GkkVwp5/Qy2ZI9C4M3YQJE0rdLtsNo5UzlNXT6dJl5tVTp06pwEF35ZVXqq08Z599tpqSvTwSjPz4448wmqkyH/rYbhntQkRERP5hNWPmI5ujXYiIKEDdeeedqsakvE1+FgpM1e2iT7HObhciIgpUTz75pKo3KY97V0swM1fwwW4XIiIKcA0bNlRbRYwcSeMrpux2YeaDiIjIf8wVfDi7XXIYfBAREfmNuYIPvdulgN0uRERE/mLKScaY+SAiIgqi4ENmQpMZ3Jo2baqmkf36669dPysqKsLDDz+s5raXhXdkn5tuugmHDx9GIGDNBxERURAGHzk5OWo1vOnTp5/xs9zcXLVynsxrL5cyneu2bdtw2WWXIRCw24WIiIwiM5/KsvaesJT5Mh/qqj3UVuZ7r2jO9zp16qiled3J4jb9+/dXc+K3aNHijP9TUFCgNl1mZqYriyKbN0WHOaazlYXlvP3YgUxvq5naLNhuc7XbzG0P5HbLMclU4jI81NtDRPUpyvXHD0TVOTa7h6+RP9stzyfPK79XfcVcXXXefz6f5yMjI0NFdLKwTXmeffZZtYxvWT///DNiY2O9eiyHc+XfcJzKzlOr+plN2cDQLNhu8zFr2wOx3bKUuywxn52djcLCQp88R1ZWFgKRnKjz8/NdX6qrkpeX5/G+/mq3/A7lOKUEo7i4+Izej4AIPuRFlxqQ66+/vsJZ2R599FE88MADrtvywicnJ6vlfb09k9ve45l4fsNylCAMF100CmYh0ah8KI0YMQIREREwC7bbXO02c9sDud1yHjhw4ICaGjw6Otpxp3xzL/L8RFUR+QaelZ2N+Lg49SXXIxGx0sdR5W7vvPOOmmlUsvZW6+kKhTFjxiAxMRH//Oc/1VL2sgS9lCPIAm1PP/00hg8f7tpX/p+02dNzWUxMjGvfTZs24f7771cL28kX8SuuuEKtNiuvo7T7hx9+UF/ct2zZon7nskLtJ598gpSUFGzYsEGdV1evXq1el3bt2uHNN99E37594Y3fpxznkCFDTv8+naoTOIX78o/hmmuuUS+SNLoiUVFRaitLXkxv/xElxDpeqLwiO6xh4QizevhmDRG+eE2DAdttPmZteyC2u6SkRJ0A5UTsOokX5gDPNffK45efU6/EPw8DkbYqd7v22mtx7733YsGCBRg2bJi67+TJk/jpp59U5ly+5V988cV45pln1Dnso48+wujRo1Wdo3uJgd52T1idr5EEM1LeMHDgQKxatQrHjh3DX//6V9xzzz2YMWOGyj6MGzcOt912G2bNmqVur1y5UnWDyP+/8cYb0atXL3XulfvWr1+vjtHT46jqGKVN5b3XqvPeC/dl4LFv3z61zHCgzEWvD7XVp1iPjw6sP1IiIgoM9erVUwHAzJkzXcHHF198gaSkJJx33nnqJCyDL3RTp07FnDlz8O2332LixIm1eu6ZM2eqDIMENDJyVK+flJGmzz//vAooJMsgwU+bNm3UzyXzopNszUMPPYSOHTuq25L5CDThvgo8duzYgd9++02lpwJFZLgVVmiww6KG2zL4ICLyA+n6kAyEF2oqMrOykBAf7/m3enluD+nZhTfeeENlDj799FNcd9116rmkhmXy5Mn4/vvvceTIEVX/ILUQcuKvra1bt6rARg88xODBg1V7JbNy9tlnY+zYsSo4kq426eqR826TJk3UvtLlIpmSjz/+WP3s6quvdgUpgaLaORh5wSWFI5vYs2ePui4vuAQeV111lepnkl+SpNtSU1PV5qtCo+qQVJFzhnXkcLgtEZF/SM2FdH14Y5Ngojr7e1obAqhMg5QOSIAhdSuLFi1SAYmQVWcl0yHdLnK/nAdljiujznXTp0/HkiVLMGjQIMyePRvt27fH8uXL1c8kKNq8ebPKjEjvQ+fOndWxBnXwIYGF9CXJpkdYcn3SpEk4dOiQSjkdPHgQPXv2VFGYvi1duhSBQA8+ONEYERFVRgoqpdBTvkx/9tln6NChA3r37q1+Jif+m2++GZdffrkKOmREz969e73yvJ06dVJFo1L7oZPnk4yLHINOzr0yaEPOr127dlXdNToJRqRgVUaOShs++OADBHW3y9ChQ11jjMtT2c8CATMfRETkKcl0XHLJJSqTcMMNN7julzoKmUhTsiOSVZfJNb0158a4cePwxBNPYPz48SqLcfz4cfztb39ThaSNGjXCrl27VA2I9DQ0b95cdcVIqYPMKC5dP1LvIT9r1aqVSgZI0eqVV16JQOLzeT4CTaQz1yMTjREREVXm/PPPR/369dUJXuosdC+//DJuvfVW1e0hRagyrUR1hppWRobWyqgaGW3Tr18/dVuCB3lO/ecSbEgtx4kTJ1TvwoQJE3DHHXeo2hO5TwKRo0ePqmOTzEd582n5k+mCjyg1y6kFOQXsdiEiospJV0d565PJ1OlST+FOAgB31emG0cr0GkhXTtnH10n2Q+b0kJGkZQttIyMjVRdRoDPVqralaz6Y+SAiIvIH8wUfercLMx9ERGQAKViVmUnL27p06QIzMmG3i+OSmQ8iIjKCrOw+YMCAcn8WEWAz0hrFtMFHDofaEhGRAeLj49VGp5m22yWXQ22JiAwTqEveU/V4azoNE2Y+HC9cNms+iIh8TkZf6CNGGjRooG57vAKtBwGNzCgq66B4Y9G0YGH3U7sl8JA5R/SF5WrDdMGHvrYcaz6IiHxPTo4y2ZWsf1LekNXangxlUi1Z4t1bAU0w0PzYbnk+mdhMFrerDdMFH9Gs+SAiMpRkO2SZeZkAS9b88hZZT2zhwoUYMmSIqQo3i/zYbnm+2gYepp7hlDUfRETG0VP13jxZyklQAhpZg8VMwUdYCLTbPJ1kThztQkRE5F9WsxacsuaDiIjIP0w8wymDDyIiIn8wb7cLh9oSERH5hWmDj7yiEpTYvTNZChEREXnOtMGHHoAQERGRsUwXfIRbgDCrY1IWDrclIiIynumCD5kMLtY5zSmH2xIRERnPdMGHcAUfzHwQEREZzpTBh43BBxERkd+YM/iIcswqn8tuFyIiIsOZMvg4XfPBzAcREZHRTB185HKiMSIiIsOZNPhwdLsw80FERGQ8UwYfLDglIiLyH1MGH5zng4iIyH/MPdqFmQ8iIiLDmTL4YOaDiIjIf0xd85HLglMiIiLDmXu0C4faEhERGc6kwQdHuxAREfmLOYOPKNZ8EBER+Yspg484Z7cLaz6IiIiMZ/JuF2Y+iIiIjGbqbhdmPoiIiIxnzuDD1e1SArtd8/fhEBERmYqp5/kQuUXseiEiIjKSKYOPqHArrBbHdU6xTkREZCxTBh8Wi8W1vguH2xIRERnLlMGHsLlmOWXmg4iIyEimDT5Oj3hh5oOIiCigg4+FCxfi0ksvRdOmTVX3xddff13q55qmYdKkSWjSpAliYmIwfPhw7NixA4HGpmc+ONyWiIgosIOPnJwc9OjRA9OnTy/35y+88AJeffVVvPXWW1ixYgVsNhtGjRqF/Px8BBKu70JEROQfjq//1XDhhReqrTyS9Zg2bRr+9a9/YfTo0eq+jz76CI0aNVIZkuuuu+6M/1NQUKA2XWZmprosKipSmzfpjyeXMRGOuCszt9DrzxNo3NttJmy3udpt5raz3Wx3IKjO8Vg0iRhqSLpd5syZgzFjxqjbu3fvRps2bbBu3Tr07NnTtd+5556rbr/yyitnPMbkyZMxZcqUM+6fOXMmYmNj4Ssfbrdi7QkrrmhZgnObcKIxIiKi2sjNzcXYsWORkZGBhIQE72Y+KpOamqouJdPhTm7rPyvr0UcfxQMPPFAq85GcnIyRI0dWefA1icrmzZuHESNGYEnhdqw9cQgt23bARee2Rihzb3dERATMgu02V7vN3Ha2m+0OBHrPhSe8GnzURFRUlNrKkhfUVy+qPG5cdKS6nl+sBdQvz5d8+ZoGMrbbfMzadrbbXCICrN3VORavDrVt3Lixujx69Gip++W2/rNAYeNQWyIiIr/wavDRqlUrFWTMnz+/VBpGRr0MHDgQgbi4XDZHuxARERmq2t0u2dnZ2Llzp+v2nj17sH79etSvXx8tWrTAfffdh6eeegrt2rVTwcjjjz+u5gTRi1IDL/PB4IOIiCigg4/Vq1fjvPPOc93Wi0XHjx+PGTNm4B//+IeaC+T2229Heno6zj77bPz444+Ijo5GILG5pldntwsREVFABx9Dhw5V83lUNvz2ySefVFsgY+aDiIjIP8y7tgszH0RERH5h2uCDmQ8iIiL/MG3wcXq0CzMfRERERjJt8GFzBh/MfBARERnLvMGH2yRjdjvXdiEiIjKKiYOP0wN98orY9UJERGQU0wYfUeFWWC2O6znseiEiIjKMaYMPmY/Eptd9sOiUiIjIMKYNPkSss+6D67sQEREZx9TBh8014oWZDyIiIqOYO/hwFp2y5oOIiMg4pg4+YiOdw21Z80FERGQYUwcfzHwQEREZz9TBx+nMB4MPIiIio5g6+LDpK9uy4JSIiMgwpg4+9KG2Ocx8EBERGcbUwUecs+aDQ22JiIiMY+rgI1bvdmHmg4iIyDCmDj7cV7YlIiIiY5g6+HBlPjjUloiIyDCmDj5szqG27HYhIiIyjqmDj1h9kjHOcEpERGQYUwcfca6aD2Y+iIiIjGLq4ON0zQczH0REREYxdfBhcwYfnF6diIjIOKYOPvQZTnOLSmC3a/4+HCIiIlMwdfBhc2Y+NA3IK2LXCxERkRFMHXxER1hhtTiuc64PIiIiY5g6+LBYLG51H8x8EBERGcHUwUeplW2Z+SAiIjKE6YMPm5754HBbIiIiQ5g++HBlPjjcloiIyBAMPvSJxljzQUREZAjTBx+uxeVY80FERGQIBh/OxeU4yykREZExGHxwfRciIiJDmT74cE2xzm4XIiIiQ5g++LCx4JSIiMhQpg8+ONSWiIjIWKYPPmycZIyIiMhQpg8+YjnUloiIKLiDj5KSEjz++ONo1aoVYmJi0KZNG0ydOhWarFsfgOJcQ22Z+SAiIjKC48zrRc8//zzefPNNfPjhh+jSpQtWr16NW265BXXq1ME999yDQBPrDD6Y+SAiIgrS4GPp0qUYPXo0Lr74YnW7ZcuW+Oyzz7By5UoE8gynrPkgIiIK0uBj0KBBeOedd7B9+3a0b98eGzZswOLFi/Hyyy+Xu39BQYHadJmZmeqyqKhIbd6kP57740Y6O56y873/fIGivHabAdttrnabue1sN9sdCKpzPBbNy8UYdrsd//znP/HCCy8gLCxM1YA8/fTTePTRR8vdf/LkyZgyZcoZ98+cOROxsbHwtbR8YOq6cERZNbwwgNkPIiKimsjNzcXYsWORkZGBhIQEY4OPWbNm4aGHHsKLL76oaj7Wr1+P++67T2U+xo8f71HmIzk5GWlpaVUefE2isnnz5mHEiBGIiIhQ96VlF2Dg8wtgsQB/Th4Bq9WCUFNeu82A7TZXu83cdrab7Q4Ecv5OSkryKPjwereLBB6PPPIIrrvuOnW7W7du2LdvH5599tlyg4+oqCi1lSUvqK9eVPfHrmtz9LtICFZisSIqwusvScDw5WsayNhu8zFr29luc4kIsHZX51isvki7WK2lH1a6X6Q7JhBFR1hV1kNwinUiIiLf8/rX/EsvvVTVeLRo0UJ1u6xbt051udx6660IRBaLRc1yml1Q7Fxc7swsDBEREQVw8PHaa6+pScbuvvtuHDt2DE2bNsUdd9yBSZMmIZBnOZXgQzYiIiIKsuAjPj4e06ZNU1uwsMlEY1kFnOuDiIjIAKZf26XU+i7MfBAREfmceYIPGc5yag8Ss/8sP/PBWU6JiIgMYZ7gY+9iRLzRD733vl3hFOvMfBAREfmeeYKPZr2hWcMRW3QCSN9f7uJyzHwQERH5nnmCj0gbtCa91FXL/qXlZj442oWIiMj3zBN8SNlHyiB1ad1XOviIjdQzHww+iIiIfM1cwUcLR/Bh2b+k1P22KL3mg90uREREvmau4KN5f9hhhSV9H5BxsJzRLsx8EBER+Zqpgg9ExSMjtqXj+t7T2Q+bs9slhwWnREREPmeu4ANAWlxHx5V9i8+YZCyXBadEREQ+Z7rg44QefLhnPpzdLqz5ICIi8j0TBh/toVmswMldQOaR0tOrs+aDiIjI50wXfBSHxUJr1M1xY58j+8Hp1YmIiIxjuuDDfb4PmXJd2PSCU9Z8EBER+Zw5gw/nfB+nMx/OglNmPoiIiHzOnMFH8kCZagxI2w5kHXXNcCo1H5qsfktEREQ+Y8rgAzF1gcZdHdf3LXFlPiTuyC+y+/fYiIiIQpw5gw+Rcrbjct8SRIeHwWJx3OTickRERL5l3uCj5WDH5d4lsFotiI3Q6z4YfBAREfmSeYMPvej0+FYgJ40TjRERERnEvMGHLRFo2MWt7oOLyxERERnBvMFHma6X07OcMvNBRETkS+YOPlIGn858OIfbcnE5IiIi32LwIY5uRoPwHHWVo12IiIh8y9zBR1wDIKmDzPCB7iVb1F2c5ZSIiMi3zB18iJaO+T46F25Ul1zZloiIyLcYfDiLTtvmblCXuRxqS0RE5FMMPpwznTbO24EE5DDzQURE5GMMPuIbAYltYYGGvtZtzHwQERH5GIMPt1EvZ1m3IpuZDyIiIp9i8CFanqMuBli3cp4PIiIiH2Pw4VZ02tWyB/b8TH8fDRERUUhj8CESmiIvrgXCLBpa5m7y99EQERGFNAYfTtlNzlKXHfMd830QERGRbzD4cCpoNlBdditm5oOIiMiXGHw4ac4RLx3tu4CCbH8fDhERUchi8OEUlZSCA/YGCLfYoR1Y4e/DISIiClkMPpziosKxQuukrhfvXuzvwyEiIgpZDD6cosPDsMLe0XFj7yJ/Hw4REVHIYvDhZLVasCGsm7oenroeKMz19yERERGFJAYfbk5FNsEhLREWexFwcKW/D4eIiCgkMfhwY5O6D7uj7gN7l/j7cIiIiEKST4KPQ4cO4YYbbkBiYiJiYmLQrVs3rF69GoEuNtI9+GDRKRERkS+Ee/sBT506hcGDB+O8887DDz/8gAYNGmDHjh2oV68egmHEy3I9+Di0GijKAyJi/H1YREREIcXrwcfzzz+P5ORkfPDBB677WrVqhWAQGxWGfVoj5EU3REz+MeDgaqCVY8VbIiIiCtDg49tvv8WoUaNw9dVXY8GCBWjWrBnuvvtu3HbbbeXuX1BQoDZdZqZjVdmioiK1eZP+eBU9bky49EJZcLhOb7TJ/xEluxfC3tyx5kswq6rdoYrtNle7zdx2tpvtDgTVOR6LpmmaN588OjpaXT7wwAMqAFm1ahXuvfdevPXWWxg/fvwZ+0+ePBlTpkw54/6ZM2ciNjYWRpq504oVx614rsE8XJf1AY7HdcLSdo8aegxERETBKDc3F2PHjkVGRgYSEhKMDT4iIyPRt29fLF261HXfPffco4KQZcuWeZT5kG6btLS0Kg++JlHZvHnzMGLECERERJzx8ye//xMfL9+Px/pbcNvG66GFR6P4wV1AeBSCWVXtDlVst7nabea2s91sdyCQ83dSUpJHwYfXu12aNGmCzp07l7qvU6dO+PLLL8vdPyoqSm1lyQvqqxe1oseOj3bcdzi8BWBrCEvOMUQc2wikDEIo8OVrGsjYbvMxa9vZbnOJCLB2V+dYvD7UVka6bNu2rdR927dvR0pKCoJhng+RW2gHWjpWueV8H0RERN7l9eDj/vvvx/Lly/HMM89g586dqnbjnXfewYQJExDobJFh6jKnsBhIcQYf+zjfBxERUUAHH/369cOcOXPw2WefoWvXrpg6dSqmTZuGcePGIdDFujIfJUDLsx137l8BFBf698CIiIhCiNdrPsQll1yitmBji3S8HDkFxUCDjkBsIpB7Aji8DmgxwN+HR0REFBK4tkuZScZc3S4WC7teiIiIfIDBhxubM/ORW1DiuEPvemHRKRERkdcw+HBjc898CD3zcWAFUBJYM8kREREFKwYfbmxlMx8NOwMx9YDCbODIBr8eGxERUahg8FFBzYea+NVqPZ392Mu6DyIiIm9g8OHG5sx82DWgoNjuuNNVdMq6DyIiIm9g8OEmJsKR+RDZMtxW6DOd7l8OlDjvIyIiohpj8OHGarUg1jnLqavuo1FXIKoOUJAJpG707wESERGFAAYfFazv4hrxYg07vbAcu16IiIhqjcFHBeu75OrBh+Aic0RERF7D4KOMWNcU685uF/ei0/1LAbvb/URERFRtDD4qmGisVOajcXcgMh7IzwCObvbfwREREYUABh8VZD6y3TMfYeFAi7Mc1znfBxERUa0w+PAk8+G+zguLTomIiGqFwUcZtvJqPsoGH3bnBGRERERUbQw+Khhqe0bmo0kPIMIG5J0Cjm/1z8ERERGFAAYfZeiTjJ2R+QiLAFoMcFxn3QcREVGNMfjwNPPh3vXC4IOIiKjGGHxUkPlwre3iLkWv+1gKyKq3REREVG0MPsqwOQtOcwvLmUysaS8gPAbITQOObzP82IiIiEIBg4+K1nYpL/MRHgkk93dc38euFyIioppg8FFGrGuejwqmUWfdBxERUa0w+CjDps/zUV7BaangYwnrPoiIiGqAwUcFBae5ZYfa6pr1AcKjgZxjwImdxh4cERFRCGDwUZ2aDxEeBTTv57jOrhciIqJqY/BRhk2fZKywGFpF3Sopgx2XDD6IiIiqjcFHBZkPuwYUFFewhov7Oi+s+yAiIqoWBh9lxEQ4Mh+Vdr007wuERQJZR4CTu407OCIiohDA4KMMq9Vyuui0ouG2ETFAs76nsx9ERETkMQYf5YitaritaKnXfTD4ICIiqg4GH+WwOScaq7DbpWzRKes+iIiIPMbgo7LMR0VzfQiZZt0aAWQeBNL3GXdwREREQY7BRzniXFOsV5L5iLQBzXo7rrPrhYiIyGMMPmqa+XDvemHRKRERkccYfFRS81Fp5qNU0ekiA46KiIgoNDD4qHS0SxWZj+SzAEsYkL4fSD9gzMEREREFOQYflU2xXtloFxEVBzTt5bjOrhciIiKPMPgoR6xrcbkqMh+lul64zgsREZEnGHyUI84ZfFRZ8yFS3NZ5ISIioiox+CiHPr16lTUfooXUfVgda7xkHvb9wREREQU5Bh/lsDkLTnOrqvkQ0QlAkx6O65zvg4iIqEoMPsoRq0+v7km3S6n5Plj3QURE5Pfg47nnnoPFYsF9992HYGHzdJIxXUtn3QczH0RERP4NPlatWoW3334b3bt3R3DWfHiY+WgxEIAFOLEDyEr17cEREREFOZ8FH9nZ2Rg3bhzeffdd1KtXD8HEpo928TTzEVMXaNzNcZ2jXoiIiCrlOMv6wIQJE3DxxRdj+PDheOqppyrcr6CgQG26zMxMdVlUVKQ2b9Ifr6rHjQrTXJkPT4/B2mIgwlI3omT3Itg7XIZA4mm7Qw3bba52m7ntbDfbHQiqczw+CT5mzZqFtWvXqm6Xqjz77LOYMmXKGff//PPPiI2N9cXhYd68eZX+PKNQ/g1HTn4Rvv9+LiyWqh+zcXo0BkjAsuVn/Kadh0BUVbtDFdttPmZtO9ttLvMCrN25ubke72vRNM3xNd9LDhw4gL59+6oXRa/1GDp0KHr27Ilp06Z5lPlITk5GWloaEhISvB6VyXGNGDECERERFe6XXVCMXk/9qq7/MWkYoiIcNSCVyj2JiP+0dzzPfVsBWwMECk/bHWrYbnO128xtZ7vZ7kAg5++kpCRkZGRUef72euZjzZo1OHbsGHr37u26r6SkBAsXLsTrr7+uAo2wsNMn86ioKLWVJS+or17Uqh47Iez0y1JgtyDOk+Oo0who2AU4thkRh1YCXcYg0PjyNQ1kbLf5mLXtbLe5RARYu6tzLF4PPoYNG4ZNmzaVuu+WW25Bx44d8fDDD5cKPAJVmNWCmIgw5BWVILewBIme/kcZcntss6PoNACDDyIiokDg9eAjPj4eXbt2LXWfzWZDYmLiGfcHMluUI/jweLitvsjcyrc53wcREVElOMNpFcNtPZ5ozH2mU8l+5J700ZEREREFN58NtXX3+++/I9jE6uu7VCfzYUsCGnQEjv/p6HrpdKnvDpCIiChIMfNRAZs+y2l1Mh+CU60TERFVisFHBWJd3S7VyHwILjJHRERUKQYfVWQ+qtXt4h58pP4B5J3ywZEREREFNwYfVdR85BRWs9slvhGQ2A6ABuxf7puDIyIiCmIMPioQF+XMfFS320Ufciv2suuFiIioLAYfVdV8VDfzIVqe47hk8EFERHQGBh/ervkoVfexEcjP8PKRERERBTcGH1XUfGRXd6itSGgC1G8NaHZg/wrvHxwREVEQY/BRyfTqNa75cM9+7F3kxaMiIiIKfgw+qhztUsPgQ59sTGY6JSIiIhcGHxWIcxacyqq2tcp8HF4PFGR58ciIiIiCG4OPCsS6plevYeajbjJQNwXQSoADrPsgIiLSMfioYlXbGmc+BNd5ISIiOgODjyoyH9k1zXyUKjrlfB9EREQ6Bh8eZD40Tatd5uPwWqAwx4tHR0REFLwYfFSR+SixaygottfsQeqlAHWSAXsxcGCldw+QiIgoSDH4qGKoba3rPvSuFw65JSIiUhh8VCDMakFMRC1HvAguMkdERFQKgw9PZjn1Rubj0BqgKM9LR0ZERBS8GHx4tL5LLTIfssZLfFOgpBA4uMp7B0dERBSkGHx4UHRao5VtdRaLW9cL6z6IiIgYfHgw3DanJivbumPRKRERkQuDD4/m+qhF5sN9vg8ZbluU74UjIyIiCl4MPiph09d3qU3BqUhsC8Q1AkoKHIWnREREJsbgw4OC09zaFJzqdR/seiEiIlIYfHgw1LZW83zoON8HERGRwuDDg8xHrbtdRIpb3UdxYe0fj4iIKEgx+PCg5qPWBaeiQQcgNgkoznMsNEdERGRSDD6MGGp7xnwf7HohIiLzYvDh0fTqXsh8uHe9sOiUiIhMjMGHJzUf3sh8CD3zsX8FUFLkncckIiIKMgw+jMx8NOgExNQDinKAw+u985hERERBhsGHrxeWc2e1us33wboPIiIyJwYflbDpk4x5Y6ht2anWucgcERGZFIMPoyYZ0+mZj/3LgRIvPi4REVGQYPDh0cJyJdA0zTsP2qgLEF0HKMwCUjd65zGJiIiCCIOPSsQ6JxkrtmsoLLF750GtYUCLQY7rnO+DiIhMiMGHBwWnItdbw23d6z443wcREZkQg49KhFktiI6wenfEi/t8H/uWAXYvBjVERERBgMFHFWy+GPHSuDsQlQAUZABH//De4xIREQUBBh+eru/irYnGXHUfZzmus+6DiIhMhsGHh0WnXq35EJzvg4iITMrrwcezzz6Lfv36IT4+Hg0bNsSYMWOwbds2BCufZD7cF5nbvxSwe2kkDRERkRmDjwULFmDChAlYvnw55s2bh6KiIowcORI5OTkI6syHt4OPJj2AyDgg7xRwbIt3H5uIiCiAnR5L6iU//vhjqdszZsxQGZA1a9ZgyJAhCDY21/ouXu52CQsHkgcAu+Y7htw27urdxyciIjJL8FFWRkaGuqxfv365Py8oKFCbLjMzU11KxkQ2b9IfrzqPGxNhUZdZeQVePx5r8kCE7ZoP++6FKOl9K3ylJu0OBWy3udpt5raz3Wx3IKjO8Vg0r80bfia73Y7LLrsM6enpWLy4/FEdkydPxpQpU864f+bMmYiNjYW/fbHbikVHrRjV3I6Lkr1bm1EvZweGbJ+KgvB4/Nj1dcDiCHSIiIiCTW5uLsaOHauSDgkJCf4LPu666y788MMPKvBo3ry5x5mP5ORkpKWlVXnwNYnKpA5lxIgRiIiI8Oj/vPjzdryzaC9uHZSCRy/s4NXjQUkRwl9qA0tRLopuXww06AhfqEm7QwHbba52m7ntbDfbHQjk/J2UlORR8OGzbpeJEyfiu+++w8KFCysMPERUVJTaypIX1FcvanUeOz46Ul3mFWvePx55vOT+wO7fEXFwOdC0m3cf38DXNJCx3eZj1raz3eYSEWDtrs6xeH20iyRSJPCYM2cOfv31V7Rq1QrBLNa1sq2XR7uUHXLLycaIiMgkvJ75kGG2Uq/xzTffqLk+UlNT1f116tRBTEwMgo3NOdQ2x5tru1S0yJz0gLHug4iIQpzXMx9vvvmm6u8ZOnQomjRp4tpmz56NYM585Hh7qK2uWW8gPBrIOQ6k7fDNcxAREYVy5sOH9at+ERflo0nGdOFRQPN+wN5FwD4pOm3vm+chIiIKEFzbpQqxzknGcry5qm1ZXOeFiIhMhMFHFWzO4CPXVzUfImXw6aLTEMscERERlcXgowqxzm4Xn2Y+pNslLArITgVO7vbd8xAREQUABh9VsOndLgXFvqtniYgGmvd1XOeQWyIiCnEMPjzMfBTbNRSWeHd69XK7XmTILRERUQhj8FEFmzPzIXJ9NdxWtGTdBxERmQODjyqEWS2IjnC8TDm+Gm4rmvcHrBFA5iHg1F7fPQ8REZGfMfjwgE0f8eLLotPIWKBZH8d1dr0QEVEIY/BRnREvvhxuW6rrhcEHERGFLgYfHrC5Rrz4MPNRquiUI16IiCh0MfjwQKy+uJwvaz5E8gDAEgak73dsREREIYjBhwdszsXlfLa+iy4qzrHQnGDXCxERhSgGHx6wGdXtItj1QkREIY7BRzUKTn2e+RBcZI6IiEIcgw8P2IzMfKi6Dytwag+Qccj3z0dERGQwBh+BNNRWRCcATXo6rnO+j1opLLYjLd/fR0FERGUx+PCATc98+HKSsYqmWqcaOZKRh9FvLMPUdeGYsWyfvw+HiMhrUjPzsTXd4rvFTg3A4COQRrvoUpx1H8x81Miu49m46s1l2Hk8R91+eu42fLnmoL8Pi4io1vKLSnDD/63GW1vD8OnKAwhWDD48YNPn+TCi5kO0OAuABTixE8hKNeY5Q8Smgxm4+q1lOJSeh1aJsTiroWMl4n98uRHzthz19+EREdXKtF92YN/JXHX9+Z+2Y0+a40tWsGHw4YFYozMfMXWBxt0c19n14rGlu9Jw3TvLcDKnEN2a1cFnt/XHta3tuLxXU5TYNUyYuRbLdp2AGUl6VmpgsvKLcCK7QHVL7TuRgx1Hs/DHoQys2XdKvTYLth9XQdr+E44PNyIKHJsPZ+DdRbvV9aQoDflFdjz4+XoUlzi+ZAWT0+vFU9WZD6NqPkTLc4DUjY6ul25XGfe8QerHP47gns/Wo7DEjkFtEvHOTX0RZdVgtQDPjO6MrPwS/LL1KG77aDU+u+0sdGteB6Hqt23H8cz6MDy58XcVcBSU2NVldUSEWfDSNT1xWY+mPjtOIvKcfIF69KtN6nJU54Y4K/owXtocjbX70/H2wt2YcF5bBBNmPjwQ6xpqa1DmQ3CROY/NWrkfd3+6VgUeF3RpjA9u6Yc4Z7ZKhIdZ8frYXhjYOhHZBcUY/8FK7DyWjVD9ZnTv7A04mmfBiZxCZBUUlxt4SHAhQXW92Ag0SohCi/qxaNswDp2bJKBNAxuKSjTcO2sdPlq21y/tIKLSPliyBxsPZiA+OhyTLumE+lHA4xd3UD+b9st2bDmciWDCzIcHbPokY0YGHy0GOuo+0rYB2ceBuAbGPXcQdSW8tWA3nv/xT3X7+v7JeGpMN4RJuqOM6IgwvHNTH4x9dwU2HcrATf+3Av+9axCa1Y1BqJDulNs/WoO8Ijs61rHjpRsHIzY6CpHhVkSGWdVllPO6tZzXSCffrCZ/uxkfL9+HSd9sVt1Y9w5rB4ul4v9DRL5z4GQuXvp5u7r+z4s6oWF8lLp+ec+mmP9nGn7echQPfL4e30wcjKhwx/kq0DHzUY3RLoZ2u8TWBxp1cVznVOtnsNs1PDN3qyvwuHtoGzxzefmBhy4+OgIzbumnvtkfzsjHje+tQFp2AUJBUYldZX+k0DalfizGt7ejfaN4tEqyqQCrQXwU6sREqCCsssBDyGv45OguKuDQC9wkGJHXnIiM/5L1r6//QF5RCfq3qo9r+ya7fiZfCJ65ohsSbZH4MzUL/5m3A8GCwYcHbJEGF5zqONV6uaS46qEvNuLdRXvU7X9d3An/uKCjR9/ME+Oi8PFfBqBpnWjsTsvBzR+sVEWYwe6p77ZgxZ6TqivlzXE9EVvLnKa8lvePaI8plzkC4A+X7cN9s9dXu3aEiGrn2w2HVSG4ZC6fvaLbGV8ekuKiVAAi3lm4C6v3nkQwYPBRjRlOpR/c0A9ffZG5XfOBP+cCu34F9i8HjmwA0nYA6QeAnBNAYa6kAmCWMe53frIWX649qL6hv3R1D/z1nNbVeoymdWPw8V8HqG8LfxzKxF8/XK0eN1jNXrVfBQdi2nW90K5hnNcee/yglnjlup4It1rUh6AU7BoehBOZ1MmcQkz53xZ1/W/ntUWbBuX/bY/q0hhX9m4OSU4++N8NxtYn1hBrPjwQG3G6D00+eCPDI6v9GDuPZSEtu1C9eZLiIj3rP9eDj5O7gVnXV71/eDQQEQNExDouw+V6DMLCo9H/ZBbC5nwFRNqc+zj3U//Hub+6LP8xTu8TA1j906eYmV+kAoWVe06q2oXpY3tjeOdGNXos+T18eGt/XPfOcpUxmDhzLd68oQ8iwoIrHl+z76RKyYoHRrTHiM6NUFTk3UzO6J7NkBATgbs+WaO+gd3w3gq8f3M/1I2t/t8BEXnuqe+3qACkQ6N43HFum0r3feKyzli2Kw37TuSqLumnL3dO1xCgGHx4QEZLyMmuoNiuRkt4+qErfXVyYnvj911YuP24637pe5e6AxldICdB/TK5fmzpmgVbIjDyKWD7T0BxPlCUBxTlAkX5zss8oMStZkH2kS3vVKnjkNNpE7myZR28IiyyggDFLYCRoEYWyJOiWbm06Jdlr1vd9it7/+nb2YV2fL3uMPpnFWJwVBiu6NMCySe3AUsqejzAateQkrYFlnUngPDwM/bpCgvmDMnB9N93o2Ab8Mn7yzB+SAdYI6Icxy/tlMtwuR0FhEW5XY90PK8fpWbkqyyQZOQu7NoYE3041O68Dg3x6V/Pwq0zVqmhfde8vQwf3ToAjetEI1TIF4RjmQUY2CaRxbUekm/Yr/+2U30+3j6ktWtkINXeoh3H8dXaQ+pj5tkru6lul8okREfgxat7YNx7K/Dpiv0Y2aUxzm0fuAMV+E6pRtFpQXEhcj0oOpWg49c/j6mgQyZvEhJUNKkTrQoCM/KK1Ae4bO7kzdU6yaYCkTYqILGhbcub0Lrv3YhxzjVyBnuJIwgpdgtIygQoxflZ2LRuFbp3bIswe4FzH/fN+f+Ky9x2D3LkZ7qSQseWnwGjSLLxJrkS4bxjbdX/R14xtURfJTMQS0nlNNlRNllE+LNqHJQKRiQ4iSwTrFR02y14KRvMuG5X8v/d7svXwjHho/U4npWPjo0T8O+re1RZSFpbfVLq4fM7BuLG/1uB7UezcdVbS1X9jBS1BvPJ8/uNRzBr1X7X3+NVfZrj6cu7Bs2oAX+RzzYZYSHftMUXaw6qb9uBfMILFnmFJXhsjiOjedNZKejdop5H/29w2yTcPKglZizdi398sQE/3TckYDOUDD6qMdz2ZE7lc31IIeT3m47gzd93qcpjPaC4pm9z3DGkjcpsSG2BTIcr80zIGiSOyxzsPp6tMivy//T/q5PIt3m9GAxuk6TS6vIGk1ELinSBRMU5tgpoRUXYvz8WXftdhLAI/exdTVJTIlmWUsFJ2WAl93SGRi5l0SPN7tigX3e7r9R19/1O338ypwC/bDmCgsJixEdbMaxDA8RLDY4Hj2W323E09QgaNWygJhur+DmBtKw87D2eiUgUoWmcFUnRGlBc6GhHidulO3k9ZPPDgBnJN3zpvKJlRsDykgQvESo4CbdGYFhBEcIPPuMIVlxbxOmsjXNfx2XZ+yTAKfP/nNc7hEXi+ws0PP/zLhzMKMGTb27BY5f2QNsm9St/PGvgdGfJl4MNBzNUrcy36w+7RrFJXYtd09RJVP5G37qhjxolRGeOrHpt/g6V8ZAaA/lSZbVYcPBUHsa/vxKjezbF45d0VoWQVDPTftmO/Sdz1Wv70AUdq/V/H76go8q0S0G9DJV/9fpeCEQMPjxkc414OTPzUVBcotJjby3Y5foWIKMObhiYgr8MboWGCadT0xI0dGqSoLaycyscTs8rE5Q4Lk/lFuHAyTzMOnkAs1YdQExEmPp2IYHI+R0bop7NgMhWTh5WZ9cK6vv0qSQzJCnHX7cew0+bU9XJoWPjeHx0a3/Eu72WVSkpKsLKuXNx0UUXwVpF0JUE4LslezBZirtOApMv7YybB7cqJwCTrE8BUOy2qdv5ZwYrrtvl7V+d/1/O47mx2IuAwtN1HhJnqVD0uG/WspHvtf+WK/K2kz+Hrz34T9bw0gGJ3LaEOd5X6jKszKXVbZ+wSvZ17ue8LwwW9Dx0GGHf/+x4Lrd980ss2H4sF5tTs3EspxgNNStugRV1EqLRrXk9dEmuj2NZRZi99ghyD2p4/5VvcMPA1mhWP+7048g3Af15Lfrz67ct5dynX8p9epdf2X3ctnIfW+9WLO+xnd2VBpEvSffPXq+CNyGBxpOju6rA7eV529VEWN+sP4zftx3HYxd3wtV9mrMLq5r+OJSB9xY7RvI9NaZrqQkTPSFZ8pev7Ykr31yqisRHdmmES7oH3kzFDD48FOtaXO505kOuz1yxH+8t3o2jmY4TgswYecvgVhg/sCXqxHqeZZBuGcmMyHZex4alfiYFRzIx1vytR9W6G0cy8vHj5lS1yf/rm1JP9e+N7NxI/f9gI99EJdCSrqr5W49h9b5TKhjT9W9ZH+/e1Ldar2dNSLCRnlfkmNfif1tUunJMr2and1AnRCnI9V+dw+Idabjp/eUI14rx+AVtcGPfxqe7wUqKVGBSXJCLZYsXYmD/Pgi32B1Bi+vnheVszvtVIKTvo1+63+e837lfSXEBjqdnoaS4EFEoRp1IDREoduxnL5MhlNuy+XhUs+RXUuTKyUVn/Ex+a92dW6lPPkloyXIZu4F4mcRJ7pO3mjThzIcJPBYrwi1WXKIB1k0RZYKWcgKcM4KhqjfNYsXx7CKcOJWPR+wWWKOtaN0gHg2KYoAvHfs8bgnDPe1L8MfhLGTkl8D+jQXLfolG9+R6iIuJqqTmq+wxllPDdcbm2MeqaWhzdDusK/Y6gk2PH8NSzf082KdeK8e6XLVQXGLHI19tVJ9/F3dvgmGdalZQ3zO5rpr76LVfd6qCdPkMdf8SHAgYfFRzojHJfJzKKVR9ah8u24v0XMenaeOEaNw2pLWaZdPbRVf1bZEq0yGbzLsgw0PnbUlVs9pJF40Utco29bstKkMgGZGRnRuja7OEGn3rkMmk5CQsXR4nsgvVdTkG6fppFB/tldoC6X6SY/7tz2Mq6JAUozspwpWsjhQ6ysQ6lU0e5k0ysZb8TuX3K0PWJA0/pmczn9dTeEIWgpPF8eyaBZf1aYUbzu1a7rde6WY7GZcKTdYHqmk3mwckHK9bVIK/fbZOBcVhRRY8d0U3XC2TILmyRBUEPSoYKXF0e6nLkjKXdrd9Sirf1+26BELbtm5B05RW2HYkA1sOnUJ2fiHCYEcYStDQFo6OjWxonRSj1v45/X/tbo9XjKLiYmw8cAKnsvPV/22dGIMW9aNhUftL15378ehdeM6fue5z30fv7iv7/9x+Vvb/ObsEq6TZYdGkfXL28s0QS3mXyVeihnJFL4WRGvrTdfSKrJikxujp++QD8OG8V/I0XeXKYfjftZ8CnS6p1UN8sGSv+nxPiA7HE5d2rtVj/e38duqzdfPhTDzy1Sb83/i+AZWFYvBRzcyHnJT+OWeTq/ulZWIs7hraRn1DNqJATd48siiabA+M7KCm3ZUgRIKRVXtPuWpGJOKV/sLhnRrhvA6JyCqSav5sZBRIHUWhWvdDpuMue13fKprMUqbmblo3WmVYJBhpXu/0ZXI9x0yaFb3Bj2bmq2Bj/p/HsGRnWqkuLHncs9ok4vwODXB+x0ZokeifDI4c+6RLOquunznrDuGBzzfglfk7cNPAlri6b3NVUe4PMspK5tiQ4+qRXFelYwPhg0S6Ed8c11stePXfNQfV5G+ncgtx+5A2hmaJZAj8oVN52Ho4HW+nr8fWvVbXe1jWwpAA8tp+yejazLMFBeW33KPEjmfm/on3l+wBUoGLGzTBv6/rUXHxt7epoMQt0CkvaHHeLioqwK/zf8H55w1FhATKZeuaym4q2Cpvcw+mNKzddwIfLd2DnPxCSKnVmB6N1d+oFWXrp9z/n2M7lV2A7zYewp5jmbDAjkbxkbioa0M0lxFSlR5byRm1XxVt9pISHDy4H82bNnWr63LfqnqMqp/D48eo5Xt9/4lcvDRvm7ouXVYN42v3eFJv+J9re+KS1xarIGT2qgO4rn8LBAoGHx6yObMZ6w84KuKlZmPCeW1wYdcmhn0rL48EAX85u5XaJCMjbzL5Frpwx3HVPSPrc8imftWrl1brsSX6lhlBZY4HyYIcTs9Xi7ftPZGrtvLIkLtmbsGIXMqJQY/A3cmCZnp2Q4po9eySv0mW44Wruqsg6+Nl+1Qdj2SVXvp5m5rIRybeksyMUSQTJctmywgTWdPhnRv7nC44DpCh6PJ6SXZMVteUE7YUXTdKiFabZAXld92oTrTKnMnw3IYJUUiyRXmcUZKg6+CpXBVgyIgxKW48fT1X1UWd5ihulYzZdf2S1d9oTQIGadekSzujQ+M4lbqWYvK9J3JUF6BMVOdzrnoOD4p1i4qQH5kI1En2SrZLgt2p/9uC2aulT6q7yqi+cl0vdGgsHVOekfEZNwzVVN3Bk//bghPphXh2CTBuQAs1I7E3Anmp61o3dy6aeFDXFehdz499vQn5RXac1bo+rnGbQr02ZImFv49sr/4m5TNMPmcDpWs+MD7tg4B8Y/pq3SH0a1kPd5/XFkPbNwiIb57upPD0yj7N1SbdGkt3peHnzUfVUvIywZkEE1KBLicJ2SSwSHRdj0SizfEzmQRNHqvshFvSH5mama8++CXjIpfquvOkcCQjT43Y2a1G7+SccXzycvVoXhfDJODo2BBdmtasW8gI0vaHRnVUy1RLBmTGkr3YcSzbFcyd0y4JtwxuiaHtG/q8S0ayWD9tPqqyQ2/d2Eed0AON/B4fvaiTev+8+NM2FQzIVnbkljspUpRgqmGZAEXul/eTCjDSHUGGrM5bFXl/S1DQ1JqBh686Bx2a1q7/XXdtvxZolRSnJlmTAPqy15eoRQo9Hf7oieNZBdiWmqVWG46KCFNBvNrcr4eHqZ/7+m9GhtBKUal0hcpT3X5Oazwwsn2NMrtyrDJJ3ZB2DdTEV5Id+2T5fvW5JOsHXdBVzUBkenPWHcKiHWnOKdS7e/V3/JezW+OXLcewcu9JPPj5Bnx2+1l+/cKss2gScgWQzMxM1KlTBxkZGUhIKD0ipLZk5se5ztEPETWIkqVrQk7YwaawsBDfff8DLr2kZu2uzhA8mfjqdGCSiwOn8lRUf3a7BhjaoYGhw+9q+/t2J21YuuuE6naTYE7/q5FuN+mSucpHXTIy2ueOj9eo65Jd8OQbkTfbXdOZaI+k56tAVbrajmbo1wsctzPzcTy7wPUaekoCZUdWLUYtliebZNbkPtnk9fdl2+X9LDPsSkAlgaCssyGBfm2KrKXL9JctR7HuQLpHr4eck/RAxBGcOK5Hhllgz81Az/YtkJIYp14jvWtUXjdPTmby9/vq/B2Y7hxCK6/vS9f0wFmtE+Et8oXon19tcmVOpT5NgpAmdWLKzfhJplWOS1/aQq7Lffr1vIIibF69FDde4Z/3ujecyC7A8JcXqGD9oVEd1BeeqlT3fS5dOhe8slB1dT92USdVn+jv8zczH9UQjIGHkA8eI2YNl2yBPmIn1MhrKClL2eQP+ePle9WwZ/kQfdLZJSOTU900qGWF6y9U1/ajWXhg9np1XSYO8lYq1tckCEhoHFFpil6yaJKNcwUomfkqcJUApdhuVxkMPciQS7nt79kzJdD58q5BKisgQYMUJMvvSLoQPPkmKW2WkVwSbEgAW7brUiZrkxihoMiuMogyhF8u3deTkgBFUvOyncmKP1cdPONeGZrvHoyo66pWS/5WY9SMyzInhLRro3MI7eW9mmHK6C5eD6gHtUnCj/cNweu/7lRTE0gXsQyrrx8biUIVYJSoQEMCi2KPV1EOxw+nVmHsgBaqi82ILknpBvx5cyqOZRWobG7PFnWrPSRW99T3W1XgIV1bMkusL0gNncy9IrVZL/68Ded2aKC6ZPyJwQdRDf6QH7u4M+4b3t7RJbN0ryrmlcXdZJNRSTcPbolz2zWocZdMem6hKjCVOU4Gtk5UBWihROoppPYj2KZnl7okmXzsP79sV91hUuMi3XGy+F58OSdqqZ2QCZ/kJCt1T3LS0kn2ZFDbRFUUPqxTw3K//btnAFwBSZngRG5n5xfg16WrUa95OxzJLHBkHU/m4WhWvlqKXY5RtvLISVPPJkggIrO7+nJeCAkO/j6qAy7t0RSPfrVRzSx7OEOGxVROuuOkW0K+5MgmmR/5UiWZVim2l+2JbzarwEkKK8vOpVRb0pUtUwF8s/6QmsdEXjOd/JnLTMN9W9ZTMwHLJoFzVRmnBduPq88Q2e25K7v7dG0pqX+SgOm3bcdVoDnn7sFVTtnuSww+iGpxIrrhrBRVQLdkp3TJ7FEjeeQDRTYZbVQvNtIxPYjF0VevygdlniqLxXmfc94qt9uO2SJzVaGrfEudPq530C14F8okoHxwZAe0axSPh/67QQUVV7yxFO+N74uURJuaLFDNybP1GJbvOlHqJCXzAEm904hOjXBO+wYefVuW54u2hjm/0UdUmIbP3anhouFtS6XhJUCRQnE9GNG7QvXbadkFKkASZ7dNUtP0GxUQSmbsizsHYWtqpprXQg8sJCg7HWQ4Aw6rtdxAXto9c85cnKzbAV+sPaxqhPQvATIq7Pp+ySrIqWkxu2RgFu9MUzPhyolbnw1XyOrR7RvHY/3+dPW8W45kqu0j5wrTUsPUN6U+eqfUU3MxdW6aUOrvOLewGI/N2eTKbMrcHL4knz/PX9kdI6ctVLVLr/+6Q42Y9BcGH0Re+KM+u12S2mQuDvnw+XzVATXaSLaaknS5jKyQIk4KPJf1aKpqfiRDJVmF0dOXqG+7ZUd1SXeK1DZIhqN3i7oq62MUqQeR569o/R1ZQ0ROnPKtvnOTBMPns5Hn69LUs+HPFakbBYw9rw3uHd5BBQqyTo8UtG44kK42GeUhAYhkQ3o0r1NlNkIyTdI9JhmOuZuOlBpJJb/fy3o2Vb976SbRH0u6DKVQ17GdVO8B6UKUEVKyiegIK7o3r6sCkT4p9VRGTGrj5DH/blAQIMXdMkx/4sx1ak4RKUb19eSNFWHwQeRF8s1X+lbvH9EeGw+ko8iuqYnKZNoCubQ7L7VS10tf6vdLd0so1s+EEjmZ/G/i2bjt4zXqRCcT1Mn5qE+LehjeuZEKOrxVA+QLMgTZyGHjvg5khrRvoDbJ6Hy55qCa20LqWaQ+SzYJGK7v30LN++J+0pW/RwkYZFjwdxsOl+oGktF/F3drooIOGeFUXvAi2SKZkVQ2PajbcDDdLSA5pbrcVu45qTZ3EgwYOc2AdKntTctRl/4KPITPWjx9+nS8+OKLSE1NRY8ePfDaa6+hf//+vno6ooAi6fRBbWXFGAp18m1y9u1nqTlh5MNchpIHa3F6qJBRdXec20YVcMrJXgIPyWLISKUnvt2shv1KQHFJjyaqyFaCDvfpAeKjwjGqa2OV4RjUJrHa2SoJ6mSUkD5SSLIpu9OyVRCyeu8prNl/Sj3ftX2Tz1hOwwgTz5f1vP3LJ8HH7Nmz8cADD+Ctt97CgAEDMG3aNIwaNQrbtm1Dw4bGv9BERL4k9Ri+Gr5INSdZigGtE9U2+dIumLPuoApEJAiReZtk00kBqxT+XtajmZoWwJujZiQr07ZhvNpk3hghXV3ynGblk+Dj5Zdfxm233YZbbrlF3ZYg5Pvvv8f777+PRx55pNS+BQUFanMfJ6wXEsnmTfrjeftxAx3bzXabhVnbznZX3W7pYRjXvznG9muGjYcy8fnqg1i4Iw3tG8XhUlnErWNDNRW/g0xZ7+HaOjUUppbisYfU77s6x+P1ScZkQqvY2Fh88cUXGDNmjOv+8ePHIz09Hd98802p/SdPnowpU6ac8TgzZ85Uj0NERESBLzc3F2PHjvXPJGNpaWkoKSlBo0allwKW23/++ecZ+z/66KOqi8Y985GcnIyRI0f6ZIbTefPmYcSIEUE7G15NsN1st1mYte1sN9sdCPSei6AY7RIVFaW2suQF9dWL6svHDmRst7mYtd1mbjvbbS4RAdbu6hyL16tdkpKSEBYWhqNHj5a6X243btzY209HREREQcbrwUdkZCT69OmD+fPnu+6z2+3q9sCBA739dERERBRkfNLtIjUcUmDat29fNbeHDLXNyclxjX4hIiIi8/JJ8HHttdfi+PHjmDRpkppkrGfPnvjxxx/PKEIlIiIi8/FZwenEiRPVRkREROTOvNOrERERkV8w+CAiIiJDMfggIiIiQzH4ICIiIkMx+CAiIiJDMfggIiIiQzH4ICIiIkP5fWG5sjRNq/bqeNVZCVCW/JXHDqTFeHyN7Wa7zcKsbWe72e5AoJ+39fN4UAUfWVlZ6jI5Odnfh0JEREQ1OI/XqVOn0n0smichioFkEbrDhw8jPj4eFovF61GZBDUHDhxAQkICzILtZrvNwqxtZ7vZ7kAg4YQEHk2bNoXVag2uzIcccPPmzX36HPLLCqRfmFHYbnMxa7vN3Ha221wSArDdVWU8dCw4JSIiIkMx+CAiIiJDmSr4iIqKwhNPPKEuzYTtZrvNwqxtZ7vZ7mATcAWnREREFNpMlfkgIiIi/2PwQURERIZi8EFERESGYvBBREREhjJN8DF9+nS0bNkS0dHRGDBgAFauXIlg8uyzz6Jfv35q5teGDRtizJgx2LZtW6l98vPzMWHCBCQmJiIuLg5XXnkljh49Wmqf/fv34+KLL0ZsbKx6nIceegjFxcWl9vn999/Ru3dvVUndtm1bzJgxA4HgueeeU7Pe3nfffaZo86FDh3DDDTeotsXExKBbt25YvXq16+dSKz5p0iQ0adJE/Xz48OHYsWNHqcc4efIkxo0bpyYiqlu3Lv7yl78gOzu71D4bN27EOeeco/42ZNbEF154Af5SUlKCxx9/HK1atVJtatOmDaZOnVpqrYhQaPfChQtx6aWXqpkg5T399ddfl/q5kW3873//i44dO6p95D02d+5c+KvtsmbJww8/rI7DZrOpfW666SY163Wwt72q37m7O++8U+0zbdq0oG93hTQTmDVrlhYZGam9//772ubNm7XbbrtNq1u3rnb06FEtWIwaNUr74IMPtD/++ENbv369dtFFF2ktWrTQsrOzXfvceeedWnJysjZ//nxt9erV2llnnaUNGjTI9fPi4mKta9eu2vDhw7V169Zpc+fO1ZKSkrRHH33Utc/u3bu12NhY7YEHHtC2bNmivfbaa1pYWJj2448/av60cuVKrWXLllr37t21e++9N+TbfPLkSS0lJUW7+eabtRUrVqhj/Omnn7SdO3e69nnuuee0OnXqaF9//bW2YcMG7bLLLtNatWql5eXlufa54IILtB49emjLly/XFi1apLVt21a7/vrrXT/PyMjQGjVqpI0bN069tz777DMtJiZGe/vttzV/ePrpp7XExETtu+++0/bs2aP997//1eLi4rRXXnklpNot78PHHntM++qrrySq0ubMmVPq50a1ccmSJeq9/sILL6j3/r/+9S8tIiJC27Rpk1/anp6erv5WZ8+erf3555/asmXLtP79+2t9+vQp9RjB2Paqfuc6+bm0rWnTptp//vMfLdjbXRFTBB/y5p0wYYLrdklJifrFPvvss1qwOnbsmHoDL1iwwPVHK28g+bDWbd26Ve0jf8D6m99qtWqpqamufd58800tISFBKygoULf/8Y9/aF26dCn1XNdee60KfvwlKytLa9eunTZv3jzt3HPPdQUfodzmhx9+WDv77LMr/LndbtcaN26svfjii6775PWIiopSHzhCPljktVi1apVrnx9++EGzWCzaoUOH1O033nhDq1evnuu10J+7Q4cOmj9cfPHF2q233lrqviuuuEJ9mIZqu8ueiIxs4zXXXKNec3cDBgzQ7rjjDs0IlZ2E3b94yH779u0LmbajgnYfPHhQa9asmQoc5MuHe/ARCu12F/LdLoWFhVizZo1KW7qvHyO3ly1bhmCVkZGhLuvXr68upY2SsnRvp6TVWrRo4WqnXEqKrVGjRq59Ro0apRYp2rx5s2sf98fQ9/HnayXdKtJtUva4QrnN3377Lfr27Yurr75adRX16tUL7777ruvne/bsQWpqaqnjljUVpEvRve2SmpXH0cn+8v5fsWKFa58hQ4YgMjKyVNulS+/UqVMw2qBBgzB//nxs375d3d6wYQMWL16MCy+8MKTb7c7INgbie7+8zzrpgpD2hnLb7XY7brzxRtUt3KVLlzN+HmrtDvngIy0tTfUju598hNyWP/BgJG9SqXsYPHgwunbtqu6TtsgbTv8DLa+dclne66D/rLJ95GSdl5cHo82aNQtr165VNS9lhWqbxe7du/Hmm2+iXbt2+Omnn3DXXXfhnnvuwYcffljq2Ct7X8ulBC7uwsPDVcBandfHSI888giuu+46FURGRESooEve69LPHcrtdmdkGyvax9+vgXtNl9SAXH/99a4F1EK17c8//7xqh/ydlyfU2h1wq9qSZ5mAP/74Q30jDGWyXPS9996LefPmqcIoM5EAU77hPPPMM+q2nITld/7WW29h/PjxCFWff/45Pv30U8ycOVN9+1u/fr0KPqRIL5TbTWeSrOY111yjim8lEA9la9aswSuvvKK+aEmWxwxCPvORlJSEsLCwM0ZAyO3GjRsj2EycOBHfffcdfvvtNzRv3tx1v7RFupjS09MrbKdclvc66D+rbB/51iFV90b/QR47dkyNQpEIX7YFCxbg1VdfVdclWg+1NutklEPnzp1L3depUyc1csf92Ct7X8ulvH7uZJSPVMxX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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 14
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 测试集"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:58:39.477448Z",
     "start_time": "2025-02-26T09:58:39.033238Z"
    }
   },
   "source": [
    "model.eval()\n",
    "loss = evaluating(model, val_loader, loss_fct)\n",
    "print(f\"loss:     {loss:.4f}\")"
   ],
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "loss:     0.4639\n"
     ]
    }
   ],
   "execution_count": 15
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 拿到中间输出"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:58:42.030909Z",
     "start_time": "2025-02-26T09:58:42.012433Z"
    }
   },
   "source": [
    "logits, deep_output = model(train_ds[:][0].to(device), return_deep_output=True)"
   ],
   "outputs": [],
   "execution_count": 16
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:58:42.381333Z",
     "start_time": "2025-02-26T09:58:42.377332Z"
    }
   },
   "source": [
    "deep_output.shape"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "torch.Size([11610, 30])"
      ]
     },
     "execution_count": 17,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 17
  },
  {
   "cell_type": "code",
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-02-26T09:58:43.080453Z",
     "start_time": "2025-02-26T09:58:43.035638Z"
    }
   },
   "source": [
    "# 从这看到deep部分抽取到的特征分布，有些特征没那么重要，或许可以消减一些神经元\n",
    "plt.imshow(deep_output.cpu().detach().numpy().mean(axis=0).reshape(1, -1))\n",
    "plt.yticks([])\n",
    "plt.show()"
   ],
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 640x480 with 1 Axes>"
      ],
      "image/png": "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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 18
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "pytorch",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
